# -*- coding: utf-8 -*-
import json
import logging

from common import orm
from common.account.db.account import get_account, get_user_settings
from common.cache import redis_cache
from common.lottery import LOTTERY_TYPE
from common.lottery import db as lottery_db
from common.lottery.cyclical.abstract import activity, order
from common.lottery.cyclical.model import ACTIVITY_LOGIC, ORDER_LOGIC, ACTIVITY_MODEL
from common.lottery.db import get_user_read_stats
from common.push.handler import push_msg, MSG_TYPE
from common.timer import TIMER_EVENT_TYPE, TimerEvent, EventHandler
from common.utils import tz, EnhencedEncoder
from common.utils import track_logging

_LOGGER = track_logging.getLogger(__name__)
_TRACKER = logging.getLogger('tracker')

ACTIVITY_OFFSET = {  # 活动向前偏移，防止时钟误差
    LOTTERY_TYPE.CQ_SSC: 60,
    LOTTERY_TYPE.CQ_LF: 60,
    LOTTERY_TYPE.XJ_SSC: 60,
    LOTTERY_TYPE.TJ_SSC: 270,
    LOTTERY_TYPE.JS_KS: 90,
    LOTTERY_TYPE.SD_11X5: 60,
    LOTTERY_TYPE.GX_KS: 90,
    LOTTERY_TYPE.SH_11X5: 60,
    LOTTERY_TYPE.JX_11X5: 60,
    LOTTERY_TYPE.GD_11X5: 60,
    LOTTERY_TYPE.BJ_PK10: 30,
    LOTTERY_TYPE.TC_PLS: 60,
    LOTTERY_TYPE.FC3D: 60,
    LOTTERY_TYPE.FF_SSC: 0,
    LOTTERY_TYPE.FF_11X5: 0,
    LOTTERY_TYPE.FF_KS: 0,
    LOTTERY_TYPE.FF_PK10: 0,
}

NEXT_TERM = {
    LOTTERY_TYPE.CQ_SSC: TIMER_EVENT_TYPE.CQ_SSC_ACTIVITY_START,
    LOTTERY_TYPE.CQ_LF: TIMER_EVENT_TYPE.CQ_LF_ACTIVITY_START,
    LOTTERY_TYPE.XJ_SSC: TIMER_EVENT_TYPE.XJ_SSC_ACTIVITY_START,
    LOTTERY_TYPE.TJ_SSC: TIMER_EVENT_TYPE.TJ_SSC_ACTIVITY_START,
    LOTTERY_TYPE.JS_KS: TIMER_EVENT_TYPE.JS_KS_ACTIVITY_START,
    LOTTERY_TYPE.GX_KS: TIMER_EVENT_TYPE.GX_KS_ACTIVITY_START,
    LOTTERY_TYPE.SD_11X5: TIMER_EVENT_TYPE.SD_11X5_ACTIVITY_START,
    LOTTERY_TYPE.JX_11X5: TIMER_EVENT_TYPE.JX_11X5_ACTIVITY_START,
    LOTTERY_TYPE.SH_11X5: TIMER_EVENT_TYPE.SH_11X5_ACTIVITY_START,
    LOTTERY_TYPE.GD_11X5: TIMER_EVENT_TYPE.GD_11X5_ACTIVITY_START,
    LOTTERY_TYPE.TC_PLS: TIMER_EVENT_TYPE.TC_PLS_ACTIVITY_START,
    LOTTERY_TYPE.FC3D: TIMER_EVENT_TYPE.FC3D_ACTIVITY_START,
    LOTTERY_TYPE.FF_SSC: TIMER_EVENT_TYPE.FF_SSC_ACTIVITY_START,
    LOTTERY_TYPE.FF_11X5: TIMER_EVENT_TYPE.FF_11X5_ACTIVITY_START,
    LOTTERY_TYPE.FF_KS: TIMER_EVENT_TYPE.FF_KS_ACTIVITY_START,
    LOTTERY_TYPE.FF_PK10: TIMER_EVENT_TYPE.FF_PK10_ACTIVITY_START,
}

SEQ_NEXT_TERM = {
    LOTTERY_TYPE.BJ_PK10: TIMER_EVENT_TYPE.BJ_PK10_ACTIVITY_START,
}


def manual_start(activity_type, handler):
    _LOGGER.info('manual start activity, type: %s', activity_type)
    try:
        now = tz.now_ts()
        term = handler.logic.calc_term_by_ts(now)
        start_ts = handler.logic.calc_start_ts_by_term(term)
        handler.process({
            'exec_ts': start_ts - ACTIVITY_OFFSET[activity_type],
            'term': term
        })
    except Exception as e:
        _LOGGER.exception(e)


def init_activity(activity_type, handler_impl):
    """查看redis中是否有启动下一期的定时事件，如果没有，证明当前没有任何进行中的活动。
        手动计算出当前应有期数并开启。
    """
    events = redis_cache.event_exists(NEXT_TERM.get(activity_type) or SEQ_NEXT_TERM.get(activity_type))
    if not events:
        _LOGGER.info('no any activity %s ongoing!!!!' % activity_type)
        manual_start(activity_type, handler_impl)


class ActivityNextHandler(EventHandler):
    def __init__(self, activity_type):
        self.activity_type = activity_type
        self.activity_desc = LOTTERY_TYPE.get_label(activity_type)
        self.logic = ACTIVITY_LOGIC[activity_type]
        self.offset = ACTIVITY_OFFSET[activity_type]
        init_activity(activity_type, self)

    def process(self, event_msg):
        _LOGGER.info('start next %s Event[%s]' % (self.activity_desc, event_msg))
        try:
            # check activity switch in redis
            off = redis_cache.get_activity_switch(self.activity_type)
            try:
                if lottery_db.check_stop_lottery(activity=self.activity_type):
                    off = True
            except:
                _LOGGER.warn('lottery not stop crawler properly!')
            if off:
                _LOGGER.warn('activity %s has been turned down, stop next!', self.activity_type)
                return True
            exec_ts = event_msg.get('exec_ts')
            stop_ts = self.logic.calc_stop_ts(exec_ts + self.offset) - self.offset
            # 如果时间已经错过，仅创建一条不能买的记录
            real_now = tz.now_ts()
            missed = real_now >= stop_ts
            term = self.logic.calc_term_by_ts(exec_ts + self.offset)
            created = activity.create_new_term(self.activity_type, term, exec_ts, stop_ts, missed)
            if created:
                _LOGGER.info('start %s term %s, offset seconds: %s', self.activity_desc, term, real_now - exec_ts)
                # 每一期开始的时候，同时确定了本期结束和下一期开始的时间
                TimerEvent.submit(TIMER_EVENT_TYPE.ACTIVITY_STOP,
                                  {'exec_ts': stop_ts,
                                   'term': term,
                                   'activity_type': self.activity_type},
                                  stop_ts)
                TimerEvent.submit(NEXT_TERM[self.activity_type],
                                  {'exec_ts': stop_ts},
                                  stop_ts)
                order.move_to_tracked(self.activity_type, term)
            else:
                _LOGGER.warn('term %s of %s already created', term, self.activity_desc)
            return True
        except Exception as e:
            _LOGGER.exception('ActivityNextHandler process error.(%s)' % e)
        finally:
            orm.session.rollback()
            orm.session.close()
        return False


class SeqActivityNextHandler(EventHandler):
    """期数连续的活动，如北京PK10，逻辑与其他不同"""

    def __init__(self, activity_type):
        self.activity_type = activity_type
        self.activity_desc = LOTTERY_TYPE.get_label(activity_type)
        self.logic = ACTIVITY_LOGIC[activity_type]
        self.activity_model = ACTIVITY_MODEL[activity_type]
        self.offset = ACTIVITY_OFFSET[activity_type]
        init_activity(activity_type, self)

    def process(self, event_msg):
        _LOGGER.info('start next %s Event[%s]' % (self.activity_desc, event_msg))
        try:
            # check activity switch in redis
            off = redis_cache.get_activity_switch(self.activity_type)
            if off:
                _LOGGER.warn('activity %s has been turned down, stop next!', self.activity_type)
                return True
            exec_ts = event_msg.get('exec_ts')
            stop_ts = self.logic.calc_stop_ts(exec_ts + self.offset) - self.offset
            # 如果时间已经错过，仅创建一条不能买的记录
            real_now = tz.now_ts()
            missed = real_now >= stop_ts
            term = self.logic.calc_term_by_ts(exec_ts + self.offset)
            # 必须先手动爬取到最新的几期，再打开processor
            prev_10 = self.logic.plus_term(term, -10)
            # 必须保证n期之前的那一期已经爬取到结果，否则不会开新的一期
            inst = self.activity_model.query.filter(self.activity_model.term == prev_10).first()
            if not inst or not inst.number:
                # 等待10s
                next_ts = tz.now_ts() + 10
                # 应该开始的时间仍然是exec_ts
                TimerEvent.submit(SEQ_NEXT_TERM[self.activity_type], {'exec_ts': exec_ts}, next_ts)
                return True
            created = activity.create_new_term(self.activity_type, term, exec_ts, stop_ts, missed)
            if created:
                _LOGGER.info('start %s term %s, offset seconds: %s', self.activity_desc, term, real_now - exec_ts)
                # 每一期开始的时候，同时确定了本期结束和下一期开始的时间
                TimerEvent.submit(TIMER_EVENT_TYPE.ACTIVITY_STOP,
                                  {'exec_ts': stop_ts, 'term': term,
                                   'activity_type': self.activity_type},
                                  stop_ts)
                TimerEvent.submit(SEQ_NEXT_TERM[self.activity_type],
                                  {'exec_ts': stop_ts}, stop_ts)
                order.move_to_tracked(self.activity_type, term)
            return True
        except Exception as e:
            _LOGGER.exception('ActivityNextHandler process error.(%s)' % e)
        finally:
            orm.session.rollback()
            orm.session.close()
        return False


class ActivityStopHandler(EventHandler):
    def process(self, event_msg):
        _LOGGER.info('stop Activity Event[%s]' % event_msg)
        try:
            exec_ts = event_msg.get('exec_ts')
            activity_type = event_msg.get('activity_type', 1)
            activity.stop_term(activity_type, event_msg['term'])
            _LOGGER.info('stop term %s ,activity_type: %s, offset seconds: %s',
                         event_msg['term'], activity_type,
                         tz.now_ts() - exec_ts)
            return True
        except Exception as e:
            orm.session.rollback()
            _LOGGER.exception('ActivityStopHandler process error.(%s)' % e)
        finally:
            orm.session.close()
        return False


class ActivityAnnounceHandler(EventHandler):
    def process(self, event_msg):
        _LOGGER.info('Announce activity Event [%s]' % event_msg)
        try:
            activity_type = event_msg.get('activity_type', 1)
            applyer = order.WinOrderApplyer(event_msg['term'], event_msg['number'], ORDER_LOGIC[activity_type],
                                            activity_type)
            user_ids = applyer.apply()
            for user_id in user_ids:
                unread = get_user_read_stats(user_id).get('win', 0)
                enable_push = get_user_settings(get_account(user_id)).get('reward_push')
                if unread > 0 and enable_push:
                    push_msg(user_id, {
                        'type': MSG_TYPE.WIN,
                        'count': unread
                    })
            _LOGGER.info('Announced %s' % event_msg)
            return True
        except Exception as e:
            _LOGGER.exception('ActivityAnnounceHandler process error.(%s)' % e)
        finally:
            orm.session.rollback()
            orm.session.close()
        return False


from common.lottery.cyclical.cq_ssc.db.activity import insert_stats as cq_ssc_f
from common.lottery.cyclical.cq_lf.db.activity import insert_stats as cq_lf_f
from common.lottery.cyclical.gd_11x5.db.activity import \
    insert_stats as gd_11x5_f
from common.lottery.cyclical.gx_ks.db.activity import insert_stats as gx_ks_f
from common.lottery.cyclical.js_ks.db.activity import insert_stats as js_ks_f
from common.lottery.cyclical.jx_11x5.db.activity import \
    insert_stats as jx_11x5_f
from common.lottery.cyclical.sd_11x5.db.activity import \
    insert_stats as sd_11x5_f
from common.lottery.cyclical.sh_11x5.db.activity import \
    insert_stats as sh_11x5_f
from common.lottery.cyclical.tj_ssc.db.activity import insert_stats as tj_ssc_f
from common.lottery.cyclical.xj_ssc.db.activity import insert_stats as xj_ssc_f
from common.lottery.cyclical.bj_pk10.db.activity import insert_stats as bj_pk10_f
from common.lottery.cyclical.ff_pk10.db.activity import insert_stats as ff_pk10_f
from common.lottery.cyclical.tc_pls.db.activity import insert_stats as tc_pls_f
from common.lottery.cyclical.fc3d.db.activity import insert_stats as fc3d_f
from common.lottery.cyclical.ff_ssc.db.activity import insert_stats as ff_ssc_f
from common.lottery.cyclical.ff_11x5.db.activity import \
    insert_stats as ff_11x5_f
from common.lottery.cyclical.ff_ks.db.activity import insert_stats as ff_ks_f

stats_func = {
    LOTTERY_TYPE.CQ_SSC: cq_ssc_f,
    LOTTERY_TYPE.CQ_LF: cq_lf_f,
    LOTTERY_TYPE.GD_11X5: gd_11x5_f,
    LOTTERY_TYPE.GX_KS: gx_ks_f,
    LOTTERY_TYPE.JS_KS: js_ks_f,
    LOTTERY_TYPE.JX_11X5: jx_11x5_f,
    LOTTERY_TYPE.SD_11X5: sd_11x5_f,
    LOTTERY_TYPE.SH_11X5: sh_11x5_f,
    LOTTERY_TYPE.TJ_SSC: tj_ssc_f,
    LOTTERY_TYPE.XJ_SSC: xj_ssc_f,
    LOTTERY_TYPE.BJ_PK10: bj_pk10_f,
    LOTTERY_TYPE.FF_PK10: ff_pk10_f,
    LOTTERY_TYPE.TC_PLS: tc_pls_f,
    LOTTERY_TYPE.FC3D: fc3d_f,
    LOTTERY_TYPE.FF_SSC: ff_ssc_f,
    LOTTERY_TYPE.FF_11X5: ff_11x5_f,
    LOTTERY_TYPE.FF_KS: ff_ks_f,
}


class ActivityStatsHandler(EventHandler):
    ''' 由于爬虫结果有可能是乱序的，计算遗漏值的时候，必须要保证前一期的开奖结果已经爬到。
        这里如果前一期没有爬取到结果，就将事件重新放回队列。由于官方存在开奖漏期问题，
        这里必须谨防。设计延时时间为15分钟（各彩种最大开奖间隔为10分钟），采用指数退避算
        法，最大尝试次数为13次，如果13次之后第N-1期仍未爬取到结果,且第N+1期已经爬取到结
        果，认定N-1期遗漏，第N期遗漏与第N-2期对接。第N-2期仍然可能不存在，重复上述过程。

        注意上述逻辑要求爬虫尽量全量爬取，或者按顺序爬取…河内一分彩和河内五分彩的爬虫源
        不在同一页，所以这个逻辑可能会导致bug，需要谨慎处理。

        FIXME: 北京PK10必须改变程序设计，先爬到期数，然后再开始（比官方迟开，早结束），
        否则一旦漏期会出现非常严重的问题。
        TODO: 漏期后可能需要人工确认后手动退款
    '''
    MAX_TRY = 13

    def process(self, event_msg):
        try:
            term = str(event_msg['term'])
            number = event_msg['number']
            prev_term = event_msg.get('prev_term')
            activity_type = event_msg.get('activity_type', 1)
            if not prev_term:
                prev_term = ACTIVITY_LOGIC[activity_type].plus_term(term, -1)
                event_msg['prev_term'] = prev_term
            last_stats = activity.get_stats_by_term(activity_type, prev_term)
            if last_stats is not None:
                func = stats_func[activity_type]
                func(term, number, last_stats)
                return True
            else:
                return False
        except Exception as e:
            orm.session.rollback()
            _LOGGER.exception('ActivityStatsHandler process error.(%s)' % e)
        finally:
            orm.session.close()
        return False

    def max_try_callback(self, event_msg):
        """达到最大限制，将prev_term减一并重新提交事件"""
        msg = event_msg['msg']
        prev_term = msg.get('prev_term') or msg.get('term')
        ac_type = msg.get('activity_type', 1)
        event_msg['try_count'] = 0  # 重置try_count
        if ac_type not in (LOTTERY_TYPE.BJ_PK10, LOTTERY_TYPE.CQ_LF, LOTTERY_TYPE.FC3D, LOTTERY_TYPE.TC_PLS):
            msg['prev_term'] = ACTIVITY_LOGIC[ac_type].plus_term(prev_term, -1)
        redis_cache.submit_timer_event(TIMER_EVENT_TYPE.CALC_STATS,
                                       json.dumps(event_msg, ensure_ascii=False, cls=EnhencedEncoder),
                                       tz.now_ts())
